# nginx的访问日志分析
# 1. 读取nginx的访问日志
# 2. 解析日志，获取ip、时间、状态码、请求路径
# 3. 获取一分钟内存在大量的404请求的ip，如果发现就
import html
from env import Env
from util import Ipc, PathUtil
import re
from collections import defaultdict
from datetime import datetime, timedelta
import os


def generate_date_strings(day):
    """
    生成最近三天的日期字符串列表，格式为 '[dd/MMM/yyyy'
    """
    date_strings = []
    now = datetime.now()
    for i in range(day):
        date = now - timedelta(days=i)
        date_strings.append(date.strftime('[%d/%b/%Y'))
    return date_strings


# 定义 SQL 注入检测的正则表达式
SQL_INJECTION_REGEX = [
    r"(\b(SELECT|UPDATE|DELETE|INSERT|DROP|ALTER|CREATE)\b.*(\bWHERE|FROM|VALUES|SET)\b)",
    r"(\bUNION\s+ALL\b)",
    r"(\bOR\s+1=1\b)",
    r"(\b--\s*$)",
    # 可以根据需要添加更多规则
]


def is_sql_injection(query):
    """
    检测查询字符串是否包含 SQL 注入风险
    :param query: 查询字符串
    :return: 如果包含 SQL 注入风险，返回 True；否则返回 False
    """
    for regex in SQL_INJECTION_REGEX:
        if re.search(regex, query, re.IGNORECASE):
            return True
    return False


XSS_KEYWORDS = [
    "<script>", "</script>", "javascript:", "alert(", "document.cookie"
]
# 定义目录遍历字符
DIRECTORY_TRAVERSAL_CHARS = ["../", "..\\"]


def generate_html_report(host_stats):
    """
    生成 HTML 报告并写入文件
    :param host_stats: 存储每个主机的统计信息
    """
    flush_interval = Env.get_flush_interval("nginx")
    html_content = f"""
         <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="refresh" content="{flush_interval}">
    """
    html_content += """
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nginx 异常访问统计报告</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f4f4f9;
            padding: 20px;
            overflow-x: hidden;
        }
        h2 {
            color: #333;
            margin-bottom: 15px;
        }
        .host-container {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            max-height: 300px; 
            overflow-y: auto; 
        }
        .host-table {
            border-collapse: collapse;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
            border-radius: 10px;
            overflow: hidden;
            width: 100%;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 12px 15px;
            text-align: left;
        }
        th {
            background-color: #007BFF;
            color: white;
            font-weight: 600;
        }
        tr:nth-child(even) {
            background-color: #f2f2f2;
        }
        tr:hover {
            background-color: #e0e0e0;
        }
        .clickable {
            cursor: pointer;
            color: #007BFF;
            text-decoration: underline;
        }
        .modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0, 0, 0, 0.4);
        }
        .modal-content {
            background-color: #fefefe;
            margin: 3% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
            max-height: 70%;
            overflow-y: auto;
            border-radius: 10px;
        }
        .modal-content table {
            width: 100%;
        }
        .close {
            color: #aaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }
        .close:hover,
        .close:focus {
            color: black;
            text-decoration: none;
            cursor: pointer;
        }
    </style>
</head>
<body>
"""

    for host, (ip_stats, request_details, thresholds) in host_stats.items():
        err404_cnt = thresholds["error_404_cnt_for_minute"]
        brute_force_threshold = thresholds.get("brute_force_threshold", 10)

        html_content += f"""
    <h2>{host} 异常访问统计报告</h2>
    <div class="host-container">
        <table class="host-table">
            <tr>
                <th>IP</th>
                <th>时间</th>
                <th>异常类型</th>
                <th>次数</th>
            </tr>
"""
        abnormal_ips = {}
        row_index = 0
        for ip, stats in ip_stats.items():
            for time, stat in stats.items():
                for anomaly_type, count in stat.items():
                    if (anomaly_type == "404" and count > err404_cnt) or \
                            (anomaly_type == "brute_force" and count > brute_force_threshold) or \
                            (anomaly_type in ["sql_injection", "xss", "directory_traversal"] and count > 0):
                        html_content += f"""
            <tr>
                <td class="clickable" onclick="showModal('{ip}', '{time}', '{anomaly_type}', {row_index})">{ip}</td>
                <td>{time}</td>
                <td>{anomaly_type}</td>
                <td>{count}</td>
            </tr>
"""
                        abnormal_ips[(ip, time, anomaly_type)] = row_index
                        row_index += 1

        html_content += """
        </table>
    </div>
"""

        for (ip, time, anomaly_type), index in abnormal_ips.items():
            details = request_details[ip][time][anomaly_type]
            detail_table = f"""
            <table>
                <tr>
                    <th>IP</th>
                    <th>时间</th>
                    <th>请求方法</th>
                    <th>请求路径</th>
                    <th>协议</th>
                    <th>状态码</th>
                </tr>
"""
            for method, path, protocol, status_code in details:
                detail_table += f"""
                <tr>
                    <td>{ip}</td>
                    <td>{time}</td>
                    <td>{method}</td>
                    <td>{path}</td>
                    <td>{protocol}</td>
                    <td>{status_code}</td>
                </tr>
"""
            detail_table += """
            </table>
"""
            html_content += f"""
    <div id="modal-{ip}-{index}" class="modal">
        <div class="modal-content">
            <span class="close" onclick="closeModal({index},'{ip}')">&times;</span>
            <h3>{ip} - {time} - {anomaly_type} 详情</h3>
            {detail_table}
        </div>
    </div>
"""

    html_content += """
    <script>
        function showModal(ip, time, anomaly_type, index) {
            const modal = document.getElementById(`modal-${ip}-${index}`);
            modal.style.display = "block";
        }

        function closeModal(index,ip) {
            const modal = document.getElementById(`modal-${ip}-${index}`);
            modal.style.display = "none";
        }

        window.onclick = function(event) {
            const modals = document.querySelectorAll('.modal');
            modals.forEach(modal => {
                if (event.target == modal) {
                    modal.style.display = "none";
                }
            });
        }
    </script>
</body>
</html>
"""

    # 将 HTML 内容写入文件
    report_path = PathUtil.get_root_path() + "/report/nginx_access_exception.html"
    report_dir = os.path.dirname(report_path)
    if not os.path.exists(report_dir):
        os.makedirs(report_dir)
    with open(report_path, 'w', encoding="utf-8") as file:
        file.write(html_content)


def scan():
    host_stats = {}
    for item in Env.get_host_list():
        host = item["host"]
        combined_log = ""
        if not item.get("nginx"):
            print(f"未找到 {host} 的 nginx 配置信息")
            continue
        nginx_config = item["nginx"]
        access_log = nginx_config["access_log"]
        max_day = nginx_config["max_day"]
        err404_cnt = nginx_config["error_404_cnt_for_minute"]
        brute_force_threshold = nginx_config.get("brute_force_threshold", 10)  # 短时间内登录尝试的阈值
        date_strings = generate_date_strings(max_day)
        # 多次使用 grep 命令获取最近三天的日志
        for date_str in date_strings:
            cmd = f'grep "\\{date_str}" "{access_log}"'
            result = Ipc.shell_call_new(host=host, cmd=cmd)
            if result is None:
                continue
            combined_log += result

        # 存储每个 IP 在不同时间的异常请求次数
        ip_stats = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
        # 存储每个 IP 在不同时间的请求详细信息
        request_details = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))

        for line in combined_log.splitlines():
            # 解析日志行中的时间信息
            time_match = re.search(r'\[(\d{2}/[a-zA-Z]{3}/\d{4}:\d{2}:\d{2}:\d{2}) \+\d{4}\]', line)
            if time_match:
                log_time_str = time_match.group(1)
                log_time = datetime.strptime(log_time_str, '%d/%b/%Y:%H:%M:%S')
                # 解析日志行，假设日志格式为常见的 Nginx 日志格式
                match = re.match(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[.*\] "(\w+) (\S+) (\S+)" (\d+) .*', line)
                if match:
                    ip = match.group(1)
                    method = match.group(2)
                    path = html.escape(match.group(3))
                    protocol = match.group(4)
                    status_code = match.group(5)
                    minute_key = log_time.strftime('%Y-%m-%d %H:%M')

                    # 判断是否在白名单中
                    has = False
                    if nginx_config.get("whitelist"):
                        for white_path in nginx_config.get("whitelist"):
                            # 如果白名单中的路径以white_path开头，就跳过
                            if path.lower().startswith(white_path):
                                has = True
                    if has:
                        continue

                    # 404 异常
                    if status_code == '404':
                        ip_stats[ip][minute_key]["404"] += 1
                        request_details[ip][minute_key]["404"].append((method, path, protocol, status_code))

                    # SQL 注入检测
                    if is_sql_injection(path.lower()):
                        ip_stats[ip][minute_key]["sql_injection"] += 1
                        request_details[ip][minute_key]["sql_injection"].append((method, path, protocol, status_code))

                    # XSS 攻击检测
                    if any(keyword.lower() in path.lower() for keyword in XSS_KEYWORDS):
                        ip_stats[ip][minute_key]["xss"] += 1
                        request_details[ip][minute_key]["xss"].append((method, path, protocol, status_code))

                    # 目录遍历攻击检测
                    if any(traversal_char in path for traversal_char in DIRECTORY_TRAVERSAL_CHARS):
                        ip_stats[ip][minute_key]["directory_traversal"] += 1
                        request_details[ip][minute_key]["directory_traversal"].append(
                            (method, path, protocol, status_code))

                    # 暴力破解检测（假设登录请求路径为 /login）
                    if path == "/login":
                        ip_stats[ip][minute_key]["brute_force"] += 1
                        request_details[ip][minute_key]["brute_force"].append((method, path, protocol, status_code))

        thresholds = {
            "error_404_cnt_for_minute": err404_cnt,
            "brute_force_threshold": brute_force_threshold
        }
        host_stats[host] = (ip_stats, request_details, thresholds)

    generate_html_report(host_stats)


if __name__ == '__main__':
    scan()
    # iscc=is_sql_injection("/webroot/decision/view/report?op=resource&resource=/com/fr/web/images/s.gif")
    # print(iscc)
